import java.net.*;
import java.io.*;

public class TCPAdapter implements ISocket {
	//Data members
	private Socket _socket = null; //TCP socket
	private String _owner = null; //Owner peer
	private String _ownerIP = null; //Owner IP
	private final int _timeout = 50; //Maximum milliseconds of no arriving data
	private TCPReader _tcpReader = null;
	private TCPWriter _tcpWriter = null;
	
	private class TCPReader {
		private byte[] _buffer = null; //Buffer of read data
		private int _currPos; //Current position of marker in buffer
		private long _lastDataArrived; //Last time when received a data from the socket
		//private final int _timeout = 50; //Maximum milliseconds of no arriving data
		private int _maxIdleTime;
		private int _mss; //MSS size of messages
		private DataInputStream _reader = null; //Reader from the socket
		
		public TCPReader(int idleTime, int mss) {
			_currPos = 0;
			_maxIdleTime = idleTime;
			_mss = mss;
			_buffer = new byte[_mss];
		}

		public synchronized void initialize() throws IOException{
			_lastDataArrived = System.currentTimeMillis();
			_reader = new DataInputStream(_socket.getInputStream());
		}
		
		public synchronized Message receive() {
			byte[] result = handleEndMessageCharacter(); //Check whether '>' was found
			//boolean timeoutPassed = false; 
			int byteRead = -1;
			
			if (result == null) { //No '>' was found
				
				byte[] readBuffer = new byte[_buffer.length];
				
				try {
					byteRead = _reader.read(readBuffer, 0, _mss - _currPos);
				} catch (IOException ex){} 
				
				if (byteRead == -1) { //Check if there was no data to read
					if (_currPos == _mss) { //Check if we already read a whole MSS
						close(); //End communication
						
						//Log error
						String srcName = SessionsManager.getNameBySource(new Source(TCPAdapter.this, _socket.getInetAddress(), _socket.getPort())); //msg.getDestPeerName();
						if (srcName == null)
							srcName = "unknown";
						
						String data = String.format("%s\nfrom:%s\nip:%s\nudp:%s\nto:%s\nip:%s\nudp:%s",
								new String(readBuffer),
								srcName, Utils.InetToString(_socket.getInetAddress()), _socket.getPort(),
								_owner, _ownerIP, _socket.getLocalPort());
										
						Proj350.logger.logOutgoing(data);
						
						return null;
					}
					
					long now = System.currentTimeMillis();
	 				
					//Check if already idle time has passed
					if (now - _lastDataArrived >= _maxIdleTime)
						close();
							
					return null;
				}
				
				//Read bytes was succeeded
				
				//Copy to buffer
				System.arraycopy(readBuffer, 0, _buffer, _currPos, byteRead);
				_currPos += byteRead;
				
				//Updates the last time counter
				_lastDataArrived = System.currentTimeMillis();
				
				result = handleEndMessageCharacter();
			}	
			
			if (result == null)
				return null;
		
			//Check if this is a valid message
			if (!Validator.isValidMessage(new String(result))) {
				close();
				
				//Log error
				String srcName = SessionsManager.getNameBySource(new Source(TCPAdapter.this, _socket.getInetAddress(), _socket.getPort())); //msg.getDestPeerName();
				if (srcName == null)
					srcName = "unknown";
				
				String data = String.format("%s\nfrom:%s\nip:%s\nudp:%s\nto:%s\nip:%s\nudp:%s",
						new String(result),
						srcName, Utils.InetToString(_socket.getInetAddress()), _socket.getPort(),
						_owner, _ownerIP, _socket.getLocalPort());
								
				Proj350.logger.logOutgoing(data);
				
				return null;
			}
		
			return new Message(result,_socket.getInetAddress(), _socket.getPort());
		}
	
		public synchronized void close() {
			try {
				_reader.close();
			} catch (IOException ex) {}
		}
	
		/**
		 * Gets a correct message from the buffer according to '>' character
		 * @return
		 */
		private byte[] handleEndMessageCharacter() {
			String bufferToString = new String(_buffer); 
			byte[] temp = new byte[_buffer.length];
			int index =  bufferToString.indexOf('>');
			
			if (index == -1) //No > was found
				return null;
			
			String message = bufferToString.substring(0, index + 1);
			
			//Shift left the leftover data 
			System.arraycopy(_buffer, index + 1, temp, 0, _mss - index - 1);
			_currPos -= (index + 1);
			_buffer = temp;
			
			return message.getBytes();
		}
		
	}
	
	
	private class TCPWriter {
		private PrintWriter _writer = null; //Writer to the socket
		
		public synchronized void initialize() throws IOException{
			_writer = new PrintWriter(_socket.getOutputStream());
		}
		
		/**
		 * Sends data by the socket
		 * @param data
		 */
		public synchronized void send(String data) {
			_writer.write(data);
			_writer.flush();
		}

		
		public synchronized void send(Message message) {
			send(new String(message.getData()));
		}
		
		public synchronized void close() {
			_writer.close();
		}
	}
	
	/**
	 * Constructor
	 * @param socket
	 * @param maxIdleTime
	 * @param MSS
	 */
	public TCPAdapter(String owner, String ownerIP, Socket socket, int maxIdleTime, int MSS) {
		_owner = owner;
		_socket = socket;
		_tcpReader = new TCPReader(maxIdleTime, MSS);
		_tcpWriter = new TCPWriter();
		_ownerIP = ownerIP;
	}
	
	/**
	 * Initialization of class members that might throw exceptions
	 * @throws IOException
	 */
	public synchronized void initialize() throws IOException{
		//_reader = new DataInputStream(_socket.getInputStream());
		_socket.setSoTimeout(_timeout);
		_tcpReader.initialize();
		_tcpWriter.initialize();
		//_writer = new PrintWriter(_socket.getOutputStream());
		
		//_lastDataArrived = System.currentTimeMillis();
	}
	
	@Override
	/**
	 * Gets a message from the socket
	 */
	public Message receive() {
		return _tcpReader.receive();
	}
		
	/**
	 * Check if the connection is still alive
	 */
	public boolean isAlive() {
		return !_socket.isClosed();
	}

	/**
	 * Close the socket
	 */
	public synchronized void close() {
		try {
			_tcpReader.close();
			//_reader.close();
			_tcpWriter.close();
			_socket.close();
		} catch (IOException ex) {}
	}
	
	/**
	 * Sends data by the socket
	 * @param data
	 */
	private  void send(String data) {
		_tcpWriter.send(data);
	}

	@Override
	public synchronized void send(Message message) {
		//Log message
		String data = new String(message.getData());
		String destName = message.getDestPeerName();
		
		if (destName == null)
			destName = "unknown";
		data += String.format("\nfrom:%s\nip:%s\ntcp:%s\nto:%s\nip:%s\ntcp:%s",
				_owner, _ownerIP, _socket.getLocalPort() ,
				destName, Utils.InetToString(message.getAddress()), message.getPort());
		
		Proj350.logger.logOutgoing(data);
		send(new String(message.getData()));
	}
	
	public String type() {
		return "TCP";
	}
	
	public InetAddress getLocalAddress() { return _socket.getLocalAddress(); }
	
	public int getLocalPort() { return _socket.getLocalPort(); }
}
